二次元ヒストグラム#

二次元ヒストグラム2D Histogram ) とは、2種類の量的変数を対象として、その分布を で表現する可視化手法です。 文字通りヒストグラムの二次元版と捉えることもできますし、 ヒートマップの一種と捉えることもできます。 ヒストグラムと同様、 ビン によって印象が大きく変わってしまうため、慎重に設定する必要があります。

データ量が非常に多く、散布図バブルチャートではマーカーが重複してしまうとき、二次元ヒストグラムの利用を考えると良いでしょう。

マンガ雑誌巻号に掲載されているマンガ作品数とマンガ作者数の関係を表現した二次元ヒストグラムを例に説明します。 二次元ヒストグラムでは、直交する二つの 位置 スケール(上図「位置①」「位置②」)によって二つの量的変数(上図「作品数」「作者数」)と対応する ビン を指定し、 その度数を で表現します。

Plotlyでは、plotly.express.density_heatmap()で二次元ヒストグラムを作成できます。

# plotly.expressモジュールをpxという名前でインポート
# 簡単にインタラクティブな図を作成するためのモジュール
import plotly.express as px

# px.density_heatmap関数を使用して、密度ヒートマップを作成
# 'df'データフレームの'col_x'カラムをx軸、'col_y'カラムをy軸に設定
# 作成した図は'fig'という変数に保存される
fig = px.density_heatmap(df, x="col_x", y="col_y")

ヒートマップと二次元ヒストグラムは同じ関数から作成可能が、以下が異なります:

  • 前者は引数xおよびyとして質的変数を指定するが、後者は量的変数を指定する

  • 前者は引数zで配色の基準となる列を指定することで度数以外も指定できるが、後者はPlotly側で度数を自動計算する

初期設定#

以降では、マンガ・アニメ・ゲームデータを可視化するための初期設定を行います。 なお、紙幅の都合のため、書籍版と一部構成が異なることにご注意ください。

Import#

必要なライブラリをImportします。

Hide code cell content
# warningsモジュールのインポート
import warnings

# データ解析や機械学習のライブラリ使用時の警告を非表示にする目的で警告を無視
# 本書の文脈では、可視化の学習に議論を集中させるために選択した
# ただし、学習以外の場面で、警告を無視する設定は推奨しない
warnings.filterwarnings("ignore")
Hide code cell content
# pathlibモジュールのインポート
# ファイルシステムのパスを扱う
from pathlib import Path

# typingモジュールからの型ヒント関連のインポート
# 関数やクラスの引数・返り値の型を注釈するためのツール
from typing import Any, Dict, List, Optional, Union

# numpy:数値計算ライブラリのインポート
# npという名前で参照可能
import numpy as np

# pandas:データ解析ライブラリのインポート
# pdという名前で参照可能
import pandas as pd

# plotly.expressのインポート
# インタラクティブなグラフ作成のライブラリ
# pxという名前で参照可能
import plotly.express as px

# plotly.graph_objectsからFigureクラスのインポート
# 型ヒントの利用を主目的とする
from plotly.graph_objects import Figure

なお、型ヒントについてはこちらを参照ください。

定数#

本Notebookで用いる定数を定義します。 なお、Pythonにおける定数の扱いについては、こちらを参照ください。

Hide code cell content
# マンガデータ保存ディレクトリのパス
DIR_CM = Path("../../data/cm/input")
# アニメデータ保存ディレクトリのパス
DIR_AN = Path("../../data/an/input")
# ゲームデータ保存ディレクトリのパス
DIR_GM = Path("../../data/gm/input")

# マンガデータの分析結果の出力先ディレクトリのパス
DIR_OUT_CM = DIR_CM.parent / "output" / Path.cwd().parts[-1] / "2d"
# アニメデータの分析結果の出力先ディレクトリのパス
DIR_OUT_AN = DIR_AN.parent / "output" / Path.cwd().parts[-1] / "2d"
# ゲームデータの分析結果の出力先ディレクトリのパス
DIR_OUT_GM = DIR_GM.parent / "output" / Path.cwd().parts[-1] / "2d"
Hide code cell content
# 読み込み対象ファイル名の定義

# マンガ各話に関するファイル
FN_CE = "cm_ce.csv"

# マンガ作品と原作者の対応関係に関するファイル
FN_CC_CRT = "cm_cc_crt.csv"

# アニメ各話に関するファイル
FN_AE = "an_ae.csv"

# アニメ作品と声優の対応関係に関するファイル
FN_AC_ACT = "an_ac_act.csv"

# ゲームパッケージとプラットフォームの対応関係に関するファイル
FN_PKG_PF = "gm_pkg_pf.csv"
Hide code cell content
# 国内主要ゲームメーカーのプラットフォームとメーカー名の対応辞書
# キー: プラットフォーム名、値: メーカー名の略称
PF2MK = {
    "プレイステーション": "ソニー",
    "プレイステーション2": "ソニー",
    "プレイステーション・ポータブル": "ソニー",
    "プレイステーション3": "ソニー",
    "プレイステーションVita": "ソニー",
    "プレイステーション4": "ソニー",
    "ゲームアーカイブス": "ソニー",
    "SG-1000": "セガ",
    "SC-3000": "セガ",
    "SEGAマーク3": "セガ",
    "セガ・マスターシステム": "セガ",
    "メガドライブ": "セガ",
    "ゲームギア": "セガ",
    "セガサターン": "セガ",
    "ドリームキャスト": "セガ",
    "ファミリーコンピュータ": "任天堂",
    "ゲームボーイ": "任天堂",
    "スーパーファミコン": "任天堂",
    "NINTENDO64": "任天堂",
    "ゲームボーイアドバンス": "任天堂",
    "ニンテンドーゲームキューブ": "任天堂",
    "ニンテンドーDS": "任天堂",
    "ニンテンドー3DS": "任天堂",
    "Wii": "任天堂",
    "WiiU": "任天堂",
    "NintendoSwitch": "任天堂",
}
Hide code cell content
# pandasのweekday関数で取得できる曜日の数値と実際の曜日名を対応させる辞書を定義
# 0:月曜日, 1:火曜日, ... , 6:日曜日
WEEKDAY2YOBI = {
    0: "月",
    1: "火",
    2: "水",
    3: "木",
    4: "金",
    5: "土",
    6: "日",
}
Hide code cell content
# plotlyの描画設定の定義

# plotlyのグラフ描画用レンダラーの定義
# Jupyter Notebook環境のグラフ表示に適切なものを選択
RENDERER = "plotly_mimetype+notebook"

関数#

以下では、本Notebookで用いる関数を定義します。

Hide code cell content
def show_fig(fig: Figure) -> None:
    """
    所定のレンダラーを用いてplotlyの図を表示
    Jupyter Bookなどの環境での正確な表示を目的とする

    Parameters
    ----------
    fig : Figure
        表示対象のplotly図

    Returns
    -------
    None
    """

    # 図の周囲の余白を設定
    # t: 上余白
    # l: 左余白
    # r: 右余白
    # b: 下余白
    fig.update_layout(margin=dict(t=25, l=25, r=25, b=25))

    # 所定のレンダラーで図を表示
    fig.show(renderer=RENDERER)
Hide code cell content
def format_cols(df: pd.DataFrame, cols_rename: Dict[str, str]) -> pd.DataFrame:
    """
    指定されたカラムのみをデータフレームから抽出し、カラム名をリネームする関数

    Parameters
    ----------
    df : pd.DataFrame
        入力データフレーム
    cols_rename : Dict[str, str]
        リネームしたいカラム名のマッピング(元のカラム名: 新しいカラム名)

    Returns
    -------
    pd.DataFrame
        カラムが抽出・リネームされたデータフレーム
    """

    # 指定されたカラムのみを抽出し、リネーム
    df = df[cols_rename.keys()].rename(columns=cols_rename)

    return df
Hide code cell content
def save_df_to_csv(df: pd.DataFrame, dir_save: Path, fn_save: str) -> None:
    """
    DataFrameをCSVファイルとして指定されたディレクトリに保存する関数

    Parameters
    ----------
    df : pd.DataFrame
        保存対象となるDataFrame
    dir_save : Path
        出力先ディレクトリのパス
    fn_save : str
        保存するCSVファイルの名前(拡張子は含めない)
    """
    # 出力先ディレクトリが存在しない場合は作成
    dir_save.mkdir(parents=True, exist_ok=True)

    # 出力先のパスを作成
    p_save = dir_save / f"{fn_save}.csv"

    # DataFrameをCSVファイルとして保存する
    df.to_csv(p_save, index=False, encoding="utf-8-sig")

    # 保存完了のメッセージを表示する
    print(f"DataFrame is saved as '{p_save}'.")

可視化例#

マンガデータ#

雑誌巻号に関する量的変数を例に、可視化手法を説明します。

Hide code cell content
# pandasのread_csv関数でCSVファイルの読み込み
df_ce = pd.read_csv(DIR_CM / FN_CE)
df_cc_crt = pd.read_csv(DIR_CM / FN_CC_CRT)
Hide code cell content
# 各マンガ作品(cc)に紐づく原作者(crt)の情報をマージ
df_cm = pd.merge(df_ce, df_cc_crt[["ccid", "crtid", "crtname"]], on="ccid", how="outer")

# 雑誌の各巻号(miname)ごとにデータを集計
df_cm = (
    df_cm.groupby(["miname"])[["ccid", "crtid", "page_end", "date", "price", "mcname"]]
    .agg(
        {
            "ccid": "nunique",  # 作品数:ユニークなccidの数
            "crtid": "nunique",  # 作者数:ユニークなcrtidの数
            "page_end": "max",  # 合計ページ数:page_endの最大値
            "date": "first",  # 発売日:dateの最初の値
            "price": "first",  # 価格:priceの最初の値
            "mcname": "first",  # 雑誌名:mcnameの最初の値
        }
    )
    .reset_index()
)

# カラム名をわかりやすく変更
df_cm = df_cm.rename(
    columns={
        "miname": "マンガ雑誌巻号名",
        "ccid": "マンガ作品数",
        "crtid": "マンガ作者数",
        "page_end": "合計ページ数",
        "date": "発売日",
        "price": "価格",
        "mcname": "マンガ雑誌名",
    }
)
Hide code cell content
# 可視化対象のDataFrameを確認
df_cm.head()
マンガ雑誌巻号名 マンガ作品数 マンガ作者数 合計ページ数 発売日 価格 マンガ雑誌名
0 週刊少年サンデー 1970年 表示号数32 12 14 284.0 1970-08-02 80.0 週刊少年サンデー
1 週刊少年サンデー 1970年 表示号数33 12 16 307.0 1970-08-09 90.0 週刊少年サンデー
2 週刊少年サンデー 1970年 表示号数34 13 17 314.0 1970-08-16 90.0 週刊少年サンデー
3 週刊少年サンデー 1970年 表示号数35 13 17 305.0 1970-08-23 90.0 週刊少年サンデー
4 週刊少年サンデー 1970年 表示号数36 13 17 305.0 1970-08-30 90.0 週刊少年サンデー
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_cm, DIR_OUT_CM, "cm")
DataFrame is saved as '../../data/cm/output/08/2d/cm.csv'.
Hide code cell source
# 二次元ヒストグラムを作成
# X軸として作品数を、Y軸として作者数を指定
fig = px.density_heatmap(df_cm, x="マンガ作品数", y="マンガ作者数")

# 二次元ヒストグラムを描画
show_fig(fig)

上図は、マンガ雑誌巻号の作品数と作者数の関係を表現した二次元ヒストグラムです。 色が明るいほど該当するマンガ雑誌巻号が多いことを表します。

散布図の例バブルチャートではマーカーの半透明化やジッタリングで重複に対応していました。 二次元ヒストグラムではX軸とY軸の ビン で区切られた領域内で集計した結果を可視化するため、上記のような対応は不要です。 ただし、ビンの分け方で印象が大きく変わってしまうというヒストグラムと同様の問題はあります。

Plotlyではnbinsx(X軸方向のビン数)とnbinsy(Y軸方向のビン数)という引数でビンを調整可能です。 試しに、ビン数を少なく設定して再度可視化してみましょう。

Hide code cell source
# X軸方向およびY軸方向のビン数を10個ずつに調整
fig.update_traces(nbinsx=10, nbinsy=10)

# 二次元ヒストグラムを再表示
show_fig(fig)

上図は、マンガ雑誌巻号の作品数と作者数の関係を表現した二次元ヒストグラムです。 直前の図と比較して、ビン幅を広めに設定しました。 これにより分布が単純化され、容易に概要を把握可能になりました。 一方で、細かい情報が削ぎ落とされてしまい、分布に関する気づきを得づらく(与えづらく)なってしまいました。 難しいところですが、今回のデータに関してはデフォルト設定のビン幅の方が適切と考えられます。

次に、マンガ雑誌別の傾向を見てみましょう。

Hide code cell source
# 二次元ヒストグラムを作成
# X軸として作品数を、Y軸として作者数を指定
# マンガ雑誌別に2列でファセットを分け、高さを調整
fig = px.density_heatmap(
    df_cm,
    x="マンガ作品数",
    y="マンガ作者数",
    facet_col="マンガ雑誌名",
    facet_col_wrap=2,
    height=600,
)

# ファセット(マンガ雑誌ごとの二次元ヒストグラム)のタイトルを簡潔にする処理
# デフォルトではタイトルは「年代=xxx」という形式になっている
# この処理は「=」で文字列を分割して「xxx」の部分だけを取り出す
fig.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1]))

# 二次元ヒストグラムを描画
show_fig(fig)

上図は、マンガ雑誌巻号の作品数と作者数の関係を、マンガ雑誌ごとに二次元ヒストグラムで表現したものです。 週刊少年ジャンプが特定の領域に集中していることがわかりやすくなりました。

アニメデータ#

アニメ各話に関する量的変数を例に、可視化手法を説明します。

Hide code cell content
# pandasのread_csv関数でCSVファイルの読み込み
df_ae = pd.read_csv(DIR_AN / FN_AE)
Hide code cell content
# acid、acnameごとにdateの最小値(初回放送日)を集計
df_an = (
    df_ae.groupby(["acid", "acname"])
    .agg({"date": "min", "aeid": "nunique"})
    .reset_index()
)

# dateをdatetime型に変換
df_an["date"] = pd.to_datetime(df_an["date"])

# acnameの文字数を取得して格納
df_an["l_acname"] = df_an["acname"].str.len()

# 可視化用に列名を変更
df_an = df_an.rename(
    columns={"date": "初回放送日", "l_acname": "作品名の文字数", "aeid": "アニメ各話数"}
)
Hide code cell content
# 可視化対象のDataFrameを確認
df_an.head()
acid acname 初回放送日 アニメ各話数 作品名の文字数
0 C10001 ギャラクシー エンジェル 2001-04-08 24 12
1 C10003 PROJECT ARMS 2001-04-08 26 12
2 C10005 探偵少年カゲマン 2001-11-28 6 8
3 C10006 Mr.Digital TOKORO the comical cartoon [第1期] 2000-10-03 120 43
4 C10008 GEAR戦士[ギアファイター] 電童 2000-10-04 38 19
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_an, DIR_OUT_AN, "an")
DataFrame is saved as '../../data/an/output/08/2d/an.csv'.
Hide code cell source
# 二次元ヒストグラムを作成
# X軸として各話数を、Y軸として作品名の文字数を指定
fig = px.density_heatmap(df_an, x="アニメ各話数", y="作品名の文字数")

# 二次元ヒストグラムを表示
show_fig(fig)

上図は、アニメ作品の各話数と作品名の文字数の関係を表した二次元ヒストグラムです。 色が明るいほど、該当するアニメ作品が多いことを表しています。

外れ値によって表示領域が広がり、解釈しづらくなってしまいました。 データをフィルタリングして再度可視化します。

Hide code cell content
# 各話数が100話以内、かつ作品名の文字数が50文字以内のアニメ作品に限定
df_an2 = df_an[
    (df_an["アニメ各話数"] <= 100) & (df_an["作品名の文字数"] <= 50)
].reset_index(drop=True)
Hide code cell content
# 可視化対象のDataFrameを確認
df_an2.head()
acid acname 初回放送日 アニメ各話数 作品名の文字数
0 C10001 ギャラクシー エンジェル 2001-04-08 24 12
1 C10003 PROJECT ARMS 2001-04-08 26 12
2 C10005 探偵少年カゲマン 2001-11-28 6 8
3 C10008 GEAR戦士[ギアファイター] 電童 2000-10-04 38 19
4 C10010 グラビテーション 2000-10-04 13 8
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_an2, DIR_OUT_AN, "an2")
DataFrame is saved as '../../data/an/output/08/2d/an2.csv'.
Hide code cell source
# 二次元ヒストグラムを作成
# X軸として各話数を、Y軸として作品名の文字数を指定
fig = px.density_heatmap(df_an2, x="アニメ各話数", y="作品名の文字数")

# 二次元ヒストグラムを表示
show_fig(fig)

上図は、アニメ作品の各話数と作品名の文字数の関係を表した二次元ヒストグラムです。 アニメ作品は、各話数が100話以内、かつ作品名が50文字以内のものを対象としています。 また、色が明るいほど該当するアニメ作品が多いことを表しています。

これまでの分析通り、各話数は1クール約13話の整数倍にピークがあるため、10-1425-2950-54のビンが明るくなっています。 また、作品名の文字数に関しては基本的に5-9あるいは10-14文字にピークがあるようです。

特に深夜アニメが多い10-14話の作品[信之, 2022]に関しては、非常に長い作品名が存在していることもわかります。

Hide code cell content
# 10-14話のアニメ作品を抽出するためのフラグ
flag = (df_an2["アニメ各話数"] >= 10) & (df_an2["アニメ各話数"] <= 14)

# 上記に該当するアニメ作品のうち、特にアニメ作品名が長いものを5作品表示
df_an2[flag].sort_values("作品名の文字数", ascending=False).head()
acid acname 初回放送日 アニメ各話数 作品名の文字数
2348 C15442 Fate / stay night [Unlinited Blade Works][第2期] 2015-04-05 13 46
2308 C15401 大図書館の羊飼い a good librarian like a good shepherd 2014-10-02 13 46
1879 C14714 境界線上のホライゾン Ⅱ Horizon on the Middle of Nowhere 2012-07-08 13 45
1451 C13504 ヨスガノソラ In solitude, where we are least alone. 2010-10-04 12 45
2274 C15364 Fate/stay night [Unlimited Blade Works] [第1期] 2014-10-05 13 45

「他媒体の原作名 + 副題」という構成のものが多いことがわかりました。 本書のスコープから外れるためこれ以上の深掘りはしませんが、興味のある方はアニメ作品名について色々な角度で調べてみると良いかもしれません。

ゲームデータ#

ゲームパッケージに関する量的変数を例に、可視化手法を説明します。

Hide code cell content
# pandasのread_csv関数でCSVファイルの読み込み
df_pkg_pf = pd.read_csv(DIR_GM / FN_PKG_PF)
Hide code cell content
# pkgidが重複しているものを可視化対象から除外
df_gm = df_pkg_pf[~df_pkg_pf["pkgid"].duplicated(keep=False)].reset_index(drop=True)

# 可視化用にdateをdatetime型に変換
df_gm["date"] = pd.to_datetime(df_gm["date"])
# dateで昇順ソートし、インデックスを新たに張替え
df_gm = df_gm.sort_values("date", ignore_index=True)

# 可視化用に列名を変更
cols_gm = {
    "pkgid": "パッケージID",
    "date": "ゲームパッケージの発売日",
    "price": "価格",
    "pfname": "プラットフォーム名",
}
df_gm = format_cols(df_gm, cols_gm)
Hide code cell content
# 可視化対象のDataFrameを確認
df_gm.head()
パッケージID ゲームパッケージの発売日 価格 プラットフォーム名
0 M735723 1982-04-25 3500.0 PC-8801
1 M735295 1982-05-25 3500.0 PC-8801
2 M735396 1982-06-25 3500.0 PC-8801
3 M735265 1982-07-25 3500.0 PC-8801
4 M735557 1982-08-01 16500.0 PC-8801
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_gm, DIR_OUT_GM, "gm")
DataFrame is saved as '../../data/gm/output/08/2d/gm.csv'.
Hide code cell source
# 二次元ヒストグラムを作成
# X軸として発売日を、Y軸として価格を指定
fig = px.density_heatmap(df_gm, x="ゲームパッケージの発売日", y="価格")

# 二次元ヒストグラムを表示
show_fig(fig)

上図は、ゲームパッケージの発売日と価格を表現した二次元ヒストグラムです。 色が明るいほど、該当するゲームパッケージが多いことを表しています。

非常に高価なゲームパッケージが存在するため、Y軸の表示領域が広がりすぎています。 一万円以内のゲームパッケージに絞って再度可視化してみましょう。

Hide code cell content
# 10000円以下のゲームパッケージに限定
df_gm2 = df_gm[df_gm["価格"] <= 10000].reset_index(drop=True)
Hide code cell content
# 可視化対象のDataFrameを確認
df_gm2.head()
パッケージID ゲームパッケージの発売日 価格 プラットフォーム名
0 M735723 1982-04-25 3500.0 PC-8801
1 M735295 1982-05-25 3500.0 PC-8801
2 M735396 1982-06-25 3500.0 PC-8801
3 M735265 1982-07-25 3500.0 PC-8801
4 M735792 1982-08-01 6500.0 PC-8801
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_gm2, DIR_OUT_GM, "gm2")
DataFrame is saved as '../../data/gm/output/08/2d/gm2.csv'.
Hide code cell source
# 二次元ヒストグラムを作成
# X軸として発売日を、Y軸として価格を指定
fig = px.density_heatmap(df_gm2, x="ゲームパッケージの発売日", y="価格")

# 二次元ヒストグラムを表示
show_fig(fig)

上図は、一万円以内で発売されたゲームパッケージの発売日と価格を表現した二次元ヒストグラムです。 色が明るいほど、該当するゲームパッケージが多いことを表しています。

ヒストグラムでの分析結果から、一部のゲームパッケージの価格は数千800円台に設定されることが多いことがわかっています。 このような価格戦略が、ゲーム業界全体でいつ頃まで採用されていたか確認してみましょう。 上図のY軸のビン幅は500円間隔であり少し粗い[1]ので、価格を1000で割った余り[2]を格納する列を追加して再度可視化してみましょう。

Hide code cell source
# 価格を1000で割った余りを格納する列を追加
df_gm2["価格%1000"] = df_gm2["価格"] % 1000

# 二次元ヒストグラムを作成
# X軸として発売日を、Y軸として価格%1000を指定
fig = px.density_heatmap(df_gm2, x="ゲームパッケージの発売日", y="価格%1000")

# 二次元ヒストグラムを表示
show_fig(fig)

上図は、ゲームパッケージの発売日と、その価格を1000で割った余りの関係を表した二次元ヒストグラムです。 当初の想定通り、ゲーム業界では 数千800円台 で多くのゲームパッケージが販売されているようです。 この傾向は1990年頃から強まり、2005年から2010年頃に薄まりつつ、それ以降継続しています[3]

この現象を読み解くヒントを得るため、メーカーごとの価格について可視化してみましょう。

Hide code cell content
# df_gm2をコピーして新たなDataFrameを作成
df_gm3 = df_gm2.copy()

# プラットフォーム名を基準にメーカー名をマッピング
df_gm3["メーカー名"] = df_gm3["プラットフォーム名"].map(PF2MK)

# メーカー名がない行を削除
df_gm3 = df_gm3.dropna(subset="メーカー名", ignore_index=True)
Hide code cell content
# 可視化対象のDataFrameを確認
df_gm3.head()
パッケージID ゲームパッケージの発売日 価格 プラットフォーム名 価格%1000 メーカー名
0 M727875 1983-07-15 4500.0 ファミリーコンピュータ 500.0 任天堂
1 M727874 1983-07-15 4500.0 ファミリーコンピュータ 500.0 任天堂
2 M727873 1983-08-27 2500.0 ファミリーコンピュータ 500.0 任天堂
3 M727872 1983-08-27 4500.0 ファミリーコンピュータ 500.0 任天堂
4 M727871 1983-09-09 4500.0 ファミリーコンピュータ 500.0 任天堂
Hide code cell content
# 可視化対象のDataFrameを保存
save_df_to_csv(df_gm3, DIR_OUT_GM, "gm3")
DataFrame is saved as '../../data/gm/output/08/2d/gm3.csv'.
Hide code cell source
# 二次元ヒストグラムを作成
# X軸として発売日を、Y軸として価格%1000を指定
# メーカー名別にファセットを分け、2列で表示
# 複数の二次元ヒストグラムを表示するため高さを調整
fig = px.density_heatmap(
    df_gm3,
    x="ゲームパッケージの発売日",
    y="価格%1000",
    facet_col="メーカー名",
    facet_col_wrap=2,
    height=600,
)

# ファセット(メーカー名ごとの二次元ヒストグラム)のタイトルを簡潔にする処理
# デフォルトではタイトルは「メーカー名=xxx」という形式になっている
# この処理は「=」で文字列を分割して「xxx」の部分だけを取り出す
fig.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1]))

# 二次元ヒストグラムを表示
show_fig(fig)

上図は、ゲームパッケージの発売日と、その価格を1000で割った余りの関係を二次元ヒストグラムを、ゲームメーカー[4]ごとに表示したものです。

ゲームパッケージの価格を800円台にする傾向はゲームメーカーにかかわらず存在していることがわかります。 本書のスコープ外となるためこれ以上の深掘りはしませんが、興味のある方は原因を調べてみると良いでしょう。